Repository navigation
GROOVY-12459: Failed AsyncChannel producer never closes the channel so for await hangs and the exception is dropped - #2987
Conversation
JMH summary — classic (commit
|
| Group | Speedup | Calibrated | n |
|---|---|---|---|
| bench | 1.664 × | 0.969 × | 124 |
| core | 1.426 × | 1.322 × | 108 |
| grails | 1.053 × | 1.015 × | 80 |
No benchmark is ≥1.5× slower than its 90-day baseline.
⚠️ Runner speed differs ≥15% from the historical baseline hardware for: bench, core-hz, grails-ad. Raw speedups are not meaningful for those parts — use the calibrated column.
Runner calibration (this run vs baseline hardware): bench 1.69× (27 rulers) · core-ag 0.97× (3 rulers) · core-hz 1.29× (3 rulers) · grails-ad 1.21× (3 rulers) · grails-ez 0.92× (3 rulers)
Baseline: dev/bench/jmh/<part>/classic/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data
JMH summary — indy (commit
|
| Group | Speedup | Calibrated | n |
|---|---|---|---|
| bench | 1.031 × | 1.024 × | 124 |
| core | 2.246 × | 2.467 × | 108 |
| grails | 1.188 × | 1.338 × | 80 |
⚠️ 2 benchmarks at least 1.5× slower than the 90-day baseline:
org.apache.groovy.bench.AryBench.groovyCS ( {"n":"1000000"} )— 2.03× slower (calibrated)org.apache.groovy.bench.StaticMethodCallIndyBench.staticChain_groovyCS— 1.55× slower (calibrated)
⚠️ Runner speed differs ≥15% from the historical baseline hardware for: core-hz. Raw speedups are not meaningful for those parts — use the calibrated column.
Runner calibration (this run vs baseline hardware): bench 1.01× (27 rulers) · core-ag 0.95× (3 rulers) · core-hz 0.85× (3 rulers) · grails-ad 0.90× (3 rulers) · grails-ez 0.88× (3 rulers)
Baseline: dev/bench/jmh/<part>/indy/data.js on gh-pages, trailing 90 days. Daily dashboard · Per-suite raw data
0b0e2b3 to
570c912
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2987 +/- ##
==================================================
+ Coverage 72.4452% 72.4638% +0.0186%
- Complexity 40687 40721 +34
==================================================
Files 1644 1644
Lines 142897 143008 +111
Branches 26877 26895 +18
==================================================
+ Hits 103522 103629 +107
- Misses 29820 29826 +6
+ Partials 9555 9553 -2
🚀 New features to boost your workflow:
|
|
Current solution borrows ideas from C#, it looks not very groovy, we can try to find better solution. using System.Threading.Channels;
var channel = Channel.CreateBounded<int>(2);
_ = Task.Run(async () =>
{
try
{
for (var i = 1; i <= 3; i++)
{
if (i == 3)
throw new InvalidOperationException("sensor failed");
await channel.Writer.WriteAsync(i * i);
}
channel.Writer.Complete();
}
catch (Exception failure)
{
channel.Writer.Complete(failure);
}
});
var seen = new List<int>();
try
{
await foreach (var value in channel.Reader.ReadAllAsync())
seen.Add(value);
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message); // sensor failed
}
// seen == [1, 4] |
|
Here is the JavaScript solution: let n = 0;
const stream = new ReadableStream({
pull(controller) {
n += 1;
if (n === 3) {
controller.error(new Error('sensor failed')); // close with the cause
return;
}
controller.enqueue(n * n);
}
});
const seen = [];
try {
for await (const value of stream) {
seen.push(value);
}
} catch (failure) {
console.log(failure.message); // sensor failed
} |
|
AI read:
|
…fore close A drain was certified once its channel had an unconditional close(), even when the producing process could throw first and leave the consumer hanging (the GROOVY shape behind apache/groovy#2987). The checker now looks for a throw reachable before the close and withholds the certificate with a skip naming it. A close only in a task's catch is now treated as conditional.
|
@paulk-asert Thank you for the careful read, and for checking the behaviour locally. The three changes you asked for are in, and the documentation points are covered as well.
|
This comment has been minimized.
This comment has been minimized.
|
Looks good to me overall. Close-with-cause is opt-in, so a plain Checked locally, merged with current master:
One thing to fix before merging: a value can be lost in if (self.isStopping() && !pending.isDone()) {
pending.cancel();
break;
}A sender can commit its transfer to Suggested fix: only break when the cancel actually wins, and otherwise fall through to the if (self.isStopping() && !pending.isDone() && pending.cancel()) {
break;
}The next iteration then exits through the Not blocking, just noting: when one input fails, |
25e9000 to
95373cc
Compare
|
@paulk-asert Thank you. The local runs are reassuring, and the lost-value race is fixed. Cancel that loses the commit
if (self.isStopping() && !pending.isDone() && pending.cancel()) {
break;
}If a sender commits between Buffered values on the other inputLeft as specified. While the other input's buffer stays non-empty,
|
…o `for await` hangs and the exception is dropped
95373cc to
ed5ed29
Compare
|



https://issues.apache.org/jira/browse/GROOVY-12459